package com.shiyu.exam.service.impl;


import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.shiyu.exam.entity.ExamRecord;
import com.shiyu.exam.entity.Paper;
import com.shiyu.exam.entity.PaperQuestion;
import com.shiyu.exam.exception.ExamException;
import com.shiyu.exam.mapper.ExamRecordMapper;
import com.shiyu.exam.mapper.PaperMapper;
import com.shiyu.exam.mapper.PaperQuestionMapper;
import com.shiyu.exam.service.PaperQuestionService;
import com.shiyu.exam.service.PaperService;
import com.shiyu.exam.vo.PaperVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;


/**
 * 试卷服务实现类
 */
@Slf4j
@Service
public class PaperServiceImpl extends ServiceImpl<PaperMapper, Paper> implements PaperService {

    @Autowired
    private PaperQuestionService paperQuestionService;

    @Autowired
    private PaperQuestionMapper paperQuestionMapper;

    @Autowired
    private ExamRecordMapper examRecordMapper;

    // 根据ID查询试卷详情
    @Override
    public Paper getPaperById(Integer id) {
        //使用自定义的Mapper方法查询-->写SQL语句
        return baseMapper.getPageById(id);
    }

    // 创建新的试卷
    @Override
    public void createPaper(PaperVo paperVo) {
        //首先，在创建试卷的时候，需要判断试卷的名称是否已经存在
        /**
         * 构造一个基于 Lambda 的 MyBatis-Plus 查询条件，用于按【试卷名称】做等值匹配。
         * 最终效果：WHERE paper.name = #{paperVo.name}（paper 为表别名，name 为字段名）。
         * 使用 LambdaQueryWrapper 的好处：
         * 1. 避免硬编码列名，编译期即可检查字段是否存在；
         * 2. 支持链式调用，后续可继续追加 .eq()/.like()/.orderBy() 等条件；
         * 3. 与 MP 内置的 Mapper.selectList()/selectOne()/selectCount() 等方法无缝结合。
         */
        LambdaQueryWrapper<Paper> eqName = new LambdaQueryWrapper<Paper>()
                .eq(Paper::getName, paperVo.getName());
        // 使用这个查询条件来进行判断，他是否存在相同的名字？
        boolean exists = baseMapper.exists(eqName);
        // 如果已经存在了
        if (exists) {
            //抛出已经存在的异常
            throw new ExamException(10086, "该试卷已经存在");
        }
        // 如果不存在，则可以正常进行创建试卷的内容
        // 创建一个paper对象
        Paper paper = new Paper();
        // 将已经有的papervo类中的内容复制到paper对象中
        BeanUtils.copyProperties(paperVo, paper);
        // 设置现在的试卷状态为草稿
        paper.setStatus("DRAFT");
        // 计算其他的没有相同的内容的属性
        //首先计算总分
        //1.获取所有的题目
        Map<Integer, BigDecimal> questions = paperVo.getQuestions();
        /**
         * 2. 计算总分
         * 说明：
         * 1. questions 是一个 Map<?, BigDecimal>，key 为题目或题目ID，value 为该题的分值。
         * 2. values()：获取 Map 中所有分值集合（Collection<BigDecimal>）。
         * 3. stream()：将集合转换为 Stream，开启函数式流水线。
         *
         * reduce(...) 是 Stream 的“聚合”操作，把一组值“归约”成一个值。
         *
         * 4. reduce(BigDecimal.ZERO, BigDecimal::add)：
         *    - 第一个参数 BigDecimal.ZERO 作为初始累加器（identity），保证空集合时返回 0；
         *    - 第二个参数 BigDecimal::add 是累加器函数，将流中每个 BigDecimal 依次相加。
         * 5. 最终返回的 totalScore 即为所有题目分值的总和，类型为 BigDecimal，避免浮点误差。
         *
         * 示例：
         * 若 questions = {1=10, 2=20, 3=30}，则 totalScore = 60
         */
        BigDecimal totalScore = questions
                .values()
                .stream()
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        //3.设置总分到paper对象中
        paper.setTotalScore(totalScore);
        //设置题目的总共数量
        paper.setQuestionCount(questions.size());
        // 可以使用代码来进行保存了，首先保存的是试卷到试卷表
        baseMapper.insert(paper);
        // 然后保存的是题目到试卷题目表--> 也是中间表
        /**
         * 将“题目-分值”Map 批量转换为试卷-题目中间表记录，并一次性入库。
         *
         * 前端传来一个 Map<String|Integer, BigDecimal> questions，key 为题目 ID，value 为该题在试卷中的分值。
         * 数据库里有一张 paper_question（试卷-题目）中间表，字段大致为：
         *   id          主键
         *   paper_id    试卷ID
         *   question_id 题目ID
         *   score       题目在本试卷中的分值
         *
         * 代码逐行解析：
         * 1. questions.entrySet().stream()
         *    获取 Map 的 EntrySet 然后流化，后续对每个 entry 做转换。
         *
         * 2. .map(entry -> { ... })
         *    把每个 Entry<K, V> 映射成一个 PaperQuestion 实体对象。
         *
         * 3. PaperQuestion paperQuestion = new PaperQuestion(
         *        paper.getId(),                    // 当前已保存的试卷主键
         *        Long.valueOf(entry.getKey()),     // 题目 ID（key 可能是 String 或 Integer，统一转 Long）
         *        entry.getValue()                  // 该题分值
         *    );
         *    构造中间表实体，注意此时 PaperQuestion 里不需要再 setId，
         *    数据库自增主键或由 MP 的 @TableId 自动生成。
         *
         * 4. .collect(Collectors.toList())
         *    把流中的全部 PaperQuestion 收集成一个 List<PaperQuestion>。
         *
         * 5. paperQuestionService.saveBatch(collect);
         *    调用 MyBatis-Plus 提供的批量插入方法，一次性把整批数据写入 paper_question 表，
         *    相比循环 save() 性能更高，数据库压力更小（通常底层一条 SQL 完成）。
         *
         * 异常与边界说明：
         * · 如果 questions 为空，collect 会得到空列表，saveBatch 不会报错，只是插入 0 条。
         * · 如果 entry.getKey() 格式非法（例如非数字字符串），Long.valueOf 会抛 NumberFormatException，
         *  建议提前做参数校验。
         * · 若 paper.getId() 为 null，说明试卷未保存或保存失败，需保证前面事务已正确提交。
         *
         * 返回结果：无返回值，数据直接落库。
         */
        // 将Map中的“题目ID-分值”批量转换成中间表实体List，并一次性写入数据库
        List<PaperQuestion> collect = questions.entrySet()
                // 获取Map的EntrySet，并流转为流
                .stream()
                // 映射为PaperQuestion实体对象
                .map(entry -> new PaperQuestion(
                        // 当前已保存的试卷ID
                        paper.getId(),
                        // 获取题目ID
                        Long.valueOf(entry.getKey()),
                        // 获取该题分值
                        entry.getValue()))
                // 收集为List
                .collect(Collectors.toList());
        // 批量写入数据库
        paperQuestionService.saveBatch(collect);
    }

    // 修改试卷的信息
    @Override
    public void updatePaper(Integer id, PaperVo paperVo) {
        // 想要更改试卷的信息，需要先判断试卷是否已经发布了，只有草稿的试卷才可以进行修改
        // 已经传入了试卷的 ID，现在可以根据id获取试卷的状态。
        Paper paper = baseMapper.selectById(id);
        //判断是否是已发布
        if ("PUBLISHED".equals(paper.getStatus())) {
            throw new ExamException(1001, "已发布的试卷无法修改");
        }
        // 如果没有被发布，那么就可以进行修改
        //在修改的时候判断这个修改的试卷名字是否已经存在了?
        /**
         * LambdaQueryWrapper<实体类> 的泛型写谁，SFunction< ?, ...> 就写谁；
         * 现在查的是数据库表 paper，所以永远传 Paper::getXxx
         *
         * LambdaQueryWrapper<T> 的 T 决定 MP 最终拼哪张表，
         * .eq(SFunction<T,?> column, Object val) 里的 column 必须能被 MP 解析成 T 对应表的字段。
         * */
        LambdaQueryWrapper<Paper> eqPaperName = new LambdaQueryWrapper<Paper>()
                /**
                 * 名字是“新输入的”，要从 VO 里取；所以需要paperVo.getName()
                 * ID是“本来就有的”，直接拿方法参数或实体字段即可，与 VO 无关。可以直接获取 id
                 */
                .eq(Paper::getName, paperVo.getName())
                .ne(Paper::getId, id);
        // 判断名字是否已经存在了
        boolean exists = baseMapper.exists(eqPaperName);
        if (exists) {
            throw new ExamException(1002, "该名称的试卷已存在");
        }
//--------------------------------------
        /**
         * 与添加时候的方法一样
         * */
        // 如果不存在，则可以进行修改
        // 将已经有的papervo类中的内容复制到paper对象中
        BeanUtils.copyProperties(paperVo, paper);
        // 计算其他的没有相同的内容的属性
        //首先计算总分
        //1.获取所有的题目
        Map<Integer, BigDecimal> questions = paperVo.getQuestions();
        BigDecimal totalScore = questions
                .values()
                .stream()
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        //3.设置总分到paper对象中
        paper.setTotalScore(totalScore);
        //设置题目的总共数量
        paper.setQuestionCount(questions.size());
        // 可以使用代码来进行保存了，首先保存的是试卷到试卷表
        baseMapper.updateById(paper);
//--------------------------------------
        // 删除中间表中原来的数据
        paperQuestionMapper.delete(
                new LambdaQueryWrapper<PaperQuestion>()
                        .eq(PaperQuestion::getPaperId, id));

//--------------------------------------
        /**
         * 与添加时候的方法一样
         * */
        // 将Map中的“题目ID-分值”批量转换成中间表实体List，并一次性写入数据库
        List<PaperQuestion> collect = questions.entrySet()
                // 获取Map的EntrySet，并流转为流
                .stream()
                // 映射为PaperQuestion实体对象
                .map(entry -> new PaperQuestion(
                        // 当前已保存的试卷ID
                        paper.getId(),
                        // 获取题目ID
                        Long.valueOf(entry.getKey()),
                        // 获取该题分值
                        entry.getValue()))
                // 收集为List
                .collect(Collectors.toList());
        // 批量写入数据库
        paperQuestionService.saveBatch(collect);
//--------------------------------------
    }

    // 删除试卷
    @Override
    public void deletePaperById(Integer id) {
        //如果删除的试卷已经发布，则不能删除
        //先查出这个ID的试卷
        Paper paper = baseMapper.selectById(id);
        if ("PUBLISHED".equals(paper.getStatus())) {
            throw new ExamException(1003, "已发布的试卷无法删除");
        }
        /**
         * 删除试卷前的“硬引用”校验：
         * 如果数据库里已经存在用这张试卷考过试的记录，则直接禁止删除，
         * 防止产生脏数据或丢失考试结果。
         *
         * 1. 查询对象
         *    LambdaQueryWrapper<ExamRecord> 只查 exam_record 表，
         *    条件：exam_id = 当前要删除的试卷主键 id。
         *
         * 2. 查询方式
         *    selectCount(...) 只返回符合条件的记录条数（Long 类型），
         *    比 selectList 更轻量，不会把整表数据拉回内存。
         */
        //1. 查询对象
        LambdaQueryWrapper<ExamRecord> eqExamId = new LambdaQueryWrapper<ExamRecord>()
                .eq(ExamRecord::getExamId, id);
        //2. 查询方式
        Long l = examRecordMapper.selectCount(eqExamId);
        if (l > 0) {
            throw new ExamException(1004, "该试卷已考过试，请勿删除");
        }
        // 否则可以进行删除试卷
        baseMapper.deleteById(id);
        //删除中间表中的数据
        paperQuestionMapper.delete(
                new LambdaQueryWrapper<PaperQuestion>()
                        .eq(PaperQuestion::getPaperId, id));
    }

    // 修改试卷状态-->草稿-->发布
    @Override
    public void updatePaperStatus(Integer id, String status) {
        // 判断考试记录中是否有正在考试的记录，如果有正在考试的记录，则不能修改
        LambdaQueryWrapper<ExamRecord> eqDoing = new LambdaQueryWrapper<ExamRecord>().eq(ExamRecord::getExamId, id)
                .eq(ExamRecord::getStatus, "进行中");
        //查看进行中的状态下是否有考试记录
        Long l = examRecordMapper.selectCount(eqDoing);
        if (l > 0) {
            throw new ExamException(1005, "该试卷有正在考试的记录，请勿修改");
        }
        // 否则可以进行修改
        /**
         * 不需要传入实体对象，因为只改一个字段，不需要把整个 Paper 对象传进去。所以是null
         * 创建一个LambdaUpdateWrapper对象，并设置修改的字段和值
         */
        baseMapper.update(null,new LambdaUpdateWrapper<Paper>()
                //指定 只更新状态字段，其他字段不动。
                .set(Paper::getStatus, status)
                //指定 根据主键 id 更新，只改这一条记录。
                .eq(Paper::getId, id)
        );
    }
}